// SPDX-License-Identifier: GPL-2.0
#include <linux/error-injection.h>
#include <linux/debugfs.h>
#include <linux/mutex.h>
#include <linux/list.h>
#include <linux/slab.h>
#include <linux/interrupt.h>

#include "trap_io.h"

#define MAX_FEI_LEN 128

struct fei_attr {
	struct list_head list;

    const char *name;

    void __iomem *virt_addr;
    phys_addr_t phys_addr;
    uint8_t access_size;

    uint8_t probability;
    atomic_t times;             /*  specifies how many times failures may happen at most.  A value of -1 means "no limit"*/

    bool is_rd_inject;      /* 是否对读注错 */
    uint64_t rd_val;        /* 读注错值 */

    bool is_wr_inject;      /* 是否对写注错 */
    uint64_t wr_val;        /* 写注错值 */

    struct dentry *dname;
};
static DEFINE_MUTEX(fei_lock);
static LIST_HEAD(fei_attr_list);
static struct dentry *fei_debugfs_dir;

static int debugfs_ul_set(void *data, u64 val)
{
	*(unsigned long *)data = val;
	return 0;
}

static int debugfs_ul_get(void *data, u64 *val)
{
	*val = *(unsigned long *)data;
	return 0;
}

DEFINE_SIMPLE_ATTRIBUTE(fops_ul, debugfs_ul_get, debugfs_ul_set, "0x%llx\n");

static void debugfs_create_ul(const char *name, umode_t mode,
			      struct dentry *parent, uint64_t *value)
{
	debugfs_create_file(name, mode, parent, value, &fops_ul);
}

static struct fei_attr *fei_attr_new(const char *name, phys_addr_t paddr, void __iomem *vaddr)
{
	struct fei_attr *attr;

	attr = kzalloc(sizeof(*attr), GFP_ATOMIC);
	if (attr) {
		attr->phys_addr = paddr;
        attr->virt_addr = vaddr;
		INIT_LIST_HEAD(&attr->list);
        attr->name = kstrdup(name+2, GFP_KERNEL);
        attr->access_size = 32;
        atomic_set(&attr->times, 0);
        attr->probability = 100;
	}
	return attr;
}

static void fei_attr_free(struct fei_attr *attr)
{
	if (attr) {
		kfree(attr->name);
		kfree(attr);
	}
}

static struct fei_attr *fei_attr_lookup_by_pa(u64 pa)
{
	struct fei_attr *attr;

	list_for_each_entry(attr, &fei_attr_list, list) {
		if (pa == attr->phys_addr)
			return attr;
	}

	return NULL;
}

static struct fei_attr *fei_attr_lookup_by_va(void __iomem *va)
{
	struct fei_attr *attr;

	list_for_each_entry(attr, &fei_attr_list, list) {
		if (va == attr->virt_addr)
			return attr;
	}

	return NULL;
}

static void fei_debugfs_add_attr(struct fei_attr *attr)
{
    umode_t mode = S_IFREG | S_IRUSR | S_IWUSR;
	struct dentry *dir;

	dir = debugfs_create_dir(attr->name, fei_debugfs_dir);

    debugfs_create_atomic_t("times", mode, dir, &attr->times);

    debugfs_create_u8("access_size", mode, dir, &attr->access_size);

    debugfs_create_bool("rd_forcing", mode, dir, &attr->is_rd_inject);
    debugfs_create_ul("rd_value", mode, dir, &attr->rd_val);

    debugfs_create_bool("wr_forcing", mode, dir, &attr->is_wr_inject);
    debugfs_create_ul("wr_value", mode, dir, &attr->wr_val);

    debugfs_create_u8("probability", mode, dir, &attr->probability);

    attr->dname = dget(dir);
}

static void fei_debugfs_remove_attr(struct fei_attr *attr)
{
	struct dentry *dir;

	dir = debugfs_lookup(attr->name, fei_debugfs_dir);
	debugfs_remove_recursive(dir);
}


static void *fei_seq_start(struct seq_file *m, loff_t *pos)
{
	mutex_lock(&fei_lock);
	return seq_list_start(&fei_attr_list, *pos);
}

static void fei_seq_stop(struct seq_file *m, void *v)
{
	mutex_unlock(&fei_lock);
}

static void *fei_seq_next(struct seq_file *m, void *v, loff_t *pos)
{
	return seq_list_next(v, &fei_attr_list, pos);
}

static int fei_seq_show(struct seq_file *m, void *v)
{
    struct fei_attr *attr = list_entry(v, struct fei_attr, list);
    char *access_type[4] = {"Byte","HWord","Word","DWord"};
    int idx = (attr->access_size >> 4);

    seq_printf(m, "[%s]0x%llx\t\n", access_type[idx], attr->phys_addr);
	return 0;
}

static const struct seq_operations fei_seq_ops = {
	.start	= fei_seq_start,
	.next	= fei_seq_next,
	.stop	= fei_seq_stop,
	.show	= fei_seq_show,
};

static int fei_open(struct inode *inode, struct file *file)
{
	return seq_open(file, &fei_seq_ops);
}

static void fei_attr_remove(struct fei_attr *attr)
{
	fei_debugfs_remove_attr(attr);
	list_del(&attr->list);
	fei_attr_free(attr);
}

static void fei_attr_remove_all(void)
{
	struct fei_attr *attr, *n;

	list_for_each_entry_safe(attr, n, &fei_attr_list, list) {
		fei_attr_remove(attr);
	}
}

static ssize_t fei_write(struct file *file, const char __user *buffer,
			 size_t count, loff_t *ppos)
{
	struct fei_attr *attr;
	char *buf, *str;
    u64 paddr;
    void __iomem *vaddr;
    int ret;

    /* cut off if it is too long */
	if (count > MAX_FEI_LEN)
		count = MAX_FEI_LEN;
	buf = kmalloc(count + 1, GFP_KERNEL);
	if (!buf)
		return -ENOMEM;

	if (copy_from_user(buf, buffer, count)) {
		ret = -EFAULT;
		goto out_free;
	}
	buf[count] = '\0';
    str = strstrip(buf);

	mutex_lock(&fei_lock);

    /* Writing just spaces will remove all injection points */
    if (str[0] == '\0') {
        fei_attr_remove_all();
        ret = count;
        goto out;
    }
    /* Writing ![address] will remove one injection point */
    if (str[0] == '!') {
        if (kstrtou64(str+1, 0, &paddr)) {
            ret = -EINVAL;
            goto out;
        }
        attr = fei_attr_lookup_by_pa(paddr);
		if (!attr) {
			ret = -ENOENT;
			goto out;
		}
		//fei_attr_remove(attr);
		ret = count;
		goto out;
    }

    if (kstrtou64(str, 0, &paddr)) {
        ret = -EINVAL;
        goto out;
    }

    vaddr = find_within_emu_io((phys_addr_t)paddr);
    if (!vaddr) {
		ret = -ERANGE;
        goto out;
    }
	if (fei_attr_lookup_by_pa(paddr)) {
		ret = -EBUSY;
		goto out;
	}
	attr = fei_attr_new(str, paddr, vaddr);
	if (!attr) {
		ret = -ENOMEM;
		goto out;
	}

    fei_debugfs_add_attr(attr);

	list_add_tail(&attr->list, &fei_attr_list);
    ret = count;
out:
	mutex_unlock(&fei_lock);
out_free:
	kfree(buf);
	return ret;
}

static const struct file_operations fei_ops = {
	.open =		fei_open,
	.read =		seq_read,
	.write =	fei_write,
	.llseek =	seq_lseek,
	.release =	seq_release,
};

static int debugfs_irq_set(void *data, u64 val)
{
    unsigned long flags;
    struct irq_desc *desc;
    int irq = -1;

    if (val > 512)
        return 0;

    rcu_read_lock();

    desc = irq_to_desc(val);
    if ((!desc) || (desc->status_use_accessors & IRQ_HIDDEN))
        goto outsparse;
    if (!desc->action)
        goto outsparse;
    irq = (int)val;
outsparse:
    rcu_read_unlock();

    if (irq == -1)
        return 0;

    local_irq_save(flags);
    generic_handle_irq(val);
    local_irq_restore(flags);

	return 0;
}

static int debugfs_irq_get(void *data, u64 *val)
{
	return 0;
}

DEFINE_SIMPLE_ATTRIBUTE(fops_triger_irq, debugfs_irq_get, debugfs_irq_set, "%lld\n");

int error_inject_debugfs_init(void)
{
	struct dentry *dir;

    dir = debugfs_create_dir("fault_injection", NULL);
    if (!dir) {
        return -ENOMEM;
    }

	/* injectable attribute is just a symlink of io_emulation/list */
	debugfs_create_symlink("injectable", dir, "../io_emulation/list");

	debugfs_create_file("inject", 0600, dir, NULL, &fei_ops);

    debugfs_create_file("triger_interrupt", 0600, dir, NULL, &fops_triger_irq);

	fei_debugfs_dir = dir;

	return 0;
}

#define atomic_dec_not_zero(v)      atomic_add_unless((v), -1, 0)

bool should_error_inject(void __iomem *virt_address, uint8_t access_size, bool is_write, uint64_t *pvalue)
{
    struct fei_attr *attr;

    attr = fei_attr_lookup_by_va(virt_address);
    if (!attr) {
        return false;
    }

	if (atomic_read(&attr->times) == 0) {
		return false;
    }

	if (attr->probability <= prandom_u32() % 100)
		return false;

    if (attr->access_size != access_size)
		return false;

	if (atomic_read(&attr->times) != -1)
		atomic_dec_not_zero(&attr->times);

    if (is_write && attr->is_wr_inject) {
        *pvalue = attr->wr_val;
        return true;
    } else if (!is_write && attr->is_rd_inject){
        *pvalue = attr->rd_val;
        return true;
    }

    return false;
}



